package com.yanyeori.framework.jira.jiraclient;

import net.sf.json.JSON;
import net.sf.json.JSONObject;
import net.sf.json.JSONSerializer;
import org.apache.commons.lang3.StringUtils;
import org.apache.http.*;
import org.apache.http.client.HttpClient;
import org.apache.http.client.methods.*;
import org.apache.http.client.utils.URIBuilder;
import org.apache.http.entity.ContentType;
import org.apache.http.entity.StringEntity;
import org.apache.http.entity.mime.HttpMultipartMode;
import org.apache.http.entity.mime.MultipartEntityBuilder;
import org.apache.http.util.EntityUtils;

import java.io.*;
import java.net.URI;
import java.net.URISyntaxException;
import java.nio.charset.StandardCharsets;
import java.util.Map;

/**
 * A simple REST client that speaks JSON.
 */
public class RestClient {

    public static final String CONTENT_TYPE = "Content-Type";
    public static final String APPLICATION_JSON = "application/json";

    private final HttpClient httpClient;
    private final ICredentials creds;
    private final URI uri;

    /**
     * Creates a REST client instance with a URI.
     *
     * @param httpclient Underlying HTTP client to use
     * @param uri        Base URI of the remote REST service
     */
    public RestClient(HttpClient httpclient, URI uri) {
        this(httpclient, null, uri);
    }

    /**
     * Creates an authenticated REST client instance with a URI.
     *
     * @param httpclient Underlying HTTP client to use
     * @param creds      Credentials to send with each request
     * @param uri        Base URI of the remote REST service
     */
    public RestClient(HttpClient httpclient, ICredentials creds, URI uri) {
        this.httpClient = httpclient;
        this.creds = creds;
        this.uri = uri;
    }

    /**
     * Build a URI from a path.
     *
     * @param path Path to append to the base URI
     * @return the full URI
     * @throws URISyntaxException when the path is invalid
     */
    public URI buildURI(String path) throws URISyntaxException {
        return buildURI(path, null);
    }

    /**
     * Build a URI from a path and query parmeters.
     *
     * @param path   Path to append to the base URI
     * @param params Map of key value pairs
     * @return the full URI
     * @throws URISyntaxException when the path is invalid
     */
    public URI buildURI(String path, Map<String, String> params) throws URISyntaxException {
        URIBuilder ub = new URIBuilder(uri);
        ub.setPath(StringUtils.defaultIfEmpty(ub.getPath(), "") + path);

        if (params != null) {
            for (Map.Entry<String, String> ent : params.entrySet())
                ub.addParameter(ent.getKey(), ent.getValue());
        }

        return ub.build();
    }

    public JSON request(HttpRequestBase req) throws RestException, IOException {
        HttpResponse resp = requestHttpResponse(req);
        HttpEntity ent = resp.getEntity();
        StringBuilder result = new StringBuilder();

        if (ent != null) {
            String encoding = null;
            if (ent.getContentEncoding() != null) {
                encoding = ent.getContentEncoding().getValue();
            }

            if (encoding == null) {
                Header contentTypeHeader = resp.getFirstHeader(CONTENT_TYPE);
                if (contentTypeHeader != null) {
                    HeaderElement[] contentTypeElements = contentTypeHeader.getElements();
                    for (HeaderElement he : contentTypeElements) {
                        NameValuePair nvp = he.getParameterByName("charset");
                        if (nvp != null) {
                            encoding = nvp.getValue();
                        }
                    }
                }
            }

            InputStreamReader isr = encoding != null ? new InputStreamReader(ent.getContent(), encoding) : new InputStreamReader(ent.getContent());
            BufferedReader br = new BufferedReader(isr);
            String line;

            while ((line = br.readLine()) != null) {
                result.append(line);
            }

            isr.close();
            br.close();
        }
        EntityUtils.consumeQuietly(ent);

        StatusLine sl = resp.getStatusLine();

        if (sl.getStatusCode() >= 300)
            throw new RestException(sl.getReasonPhrase(), sl.getStatusCode(), result.toString(), resp.getAllHeaders());

        return result.length() > 0 ? JSONSerializer.toJSON(result.toString()) : null;
    }

    public HttpResponse requestHttpResponse(HttpRequestBase req) throws IOException {
        req.addHeader("Accept", APPLICATION_JSON);

        if (creds != null)
            creds.authenticate(req);

        return httpClient.execute(req);
    }

    private JSON request(HttpEntityEnclosingRequestBase req, String payload) throws RestException, IOException {
        if (payload != null) {
            StringEntity ent = new StringEntity(payload, StandardCharsets.UTF_8);
            ent.setContentType(APPLICATION_JSON);

            req.addHeader(CONTENT_TYPE, APPLICATION_JSON);
            req.setEntity(ent);
        }

        return request(req);
    }

    private JSON request(HttpEntityEnclosingRequestBase req, File file) throws RestException, IOException {
        if (file != null) {
            req.setHeader("X-Atlassian-Token", "nocheck");
            HttpEntity httpEntity = multipartEntityBuilder().addBinaryBody("file", file).build();
            req.setEntity(httpEntity);
        }
        return request(req);
    }

    private JSON request(HttpEntityEnclosingRequestBase req, Issue.NewAttachment... attachments) throws RestException, IOException {
        if (attachments != null && attachments.length > 0) {
            req.setHeader("X-Atlassian-Token", "nocheck");
            MultipartEntityBuilder multipartEntity = multipartEntityBuilder();
            for (Issue.NewAttachment attachment : attachments) {
                String filename = attachment.getFilename();
                Object content = attachment.getContent();
                if (content instanceof byte[]) {
                    multipartEntity.addBinaryBody("file", (byte[]) content, ContentType.DEFAULT_BINARY, filename);
                } else if (content instanceof InputStream) {
                    multipartEntity.addBinaryBody("file", (InputStream) content, ContentType.DEFAULT_BINARY, filename);
                } else if (content instanceof File) {
                    multipartEntity.addBinaryBody("file", (File) content, ContentType.DEFAULT_BINARY, filename);
                } else if (content == null) {
                    throw new IllegalArgumentException("Missing content for the file " + filename);
                } else {
                    throw new IllegalArgumentException(
                            "Expected file type byte[], java.io.InputStream or java.io.File but provided " +
                                    content.getClass().getName() + " for the file " + filename);
                }
            }
            req.setEntity(multipartEntity.build());
        }
        return request(req);
    }

    private MultipartEntityBuilder multipartEntityBuilder() {
        return MultipartEntityBuilder.create()
                .setCharset(StandardCharsets.UTF_8)
                .setMode(HttpMultipartMode.BROWSER_COMPATIBLE);
    }

    private JSON request(HttpEntityEnclosingRequestBase req, JSON payload) throws RestException, IOException {
        return request(req, payload != null ? payload.toString() : null);
    }

    /**
     * Executes an HTTP DELETE with the given URI.
     *
     * @param uri Full URI of the remote endpoint
     * @return JSON-encoded result or null when there's no content returned
     * @throws RestException when an HTTP-level error occurs
     * @throws IOException   when an error reading the response occurs
     */
    public JSON delete(URI uri) throws RestException, IOException {
        return request(new HttpDelete(uri));
    }

    /**
     * Executes an HTTP DELETE with the given path.
     *
     * @param path Path to be appended to the URI supplied in the construtor
     * @return JSON-encoded result or null when there's no content returned
     * @throws RestException      when an HTTP-level error occurs
     * @throws IOException        when an error reading the response occurs
     * @throws URISyntaxException when an error occurred appending the path to the URI
     */
    public JSON delete(String path) throws RestException, IOException, URISyntaxException {
        return delete(buildURI(path));
    }

    /**
     * Executes an HTTP GET with the given URI.
     *
     * @param uri Full URI of the remote endpoint
     * @return JSON-encoded result or null when there's no content returned
     * @throws RestException when an HTTP-level error occurs
     * @throws IOException   when an error reading the response occurs
     */
    public JSON get(URI uri) throws RestException, IOException {
        return request(new HttpGet(uri));
    }

    /**
     * Executes an HTTP GET with the given path.
     *
     * @param path   Path to be appended to the URI supplied in the construtor
     * @param params Map of key value pairs
     * @return JSON-encoded result or null when there's no content returned
     * @throws RestException      when an HTTP-level error occurs
     * @throws IOException        when an error reading the response occurs
     * @throws URISyntaxException when an error occurred appending the path to the URI
     */
    public JSON get(String path, Map<String, String> params) throws RestException, IOException, URISyntaxException {
        return get(buildURI(path, params));
    }

    /**
     * Executes an HTTP GET with the given path.
     *
     * @param path Path to be appended to the URI supplied in the construtor
     * @return JSON-encoded result or null when there's no content returned
     * @throws RestException      when an HTTP-level error occurs
     * @throws IOException        when an error reading the response occurs
     * @throws URISyntaxException when an error occurred appending the path to the URI
     */
    public JSON get(String path) throws RestException, IOException, URISyntaxException {
        return get(path, null);
    }


    /**
     * Executes an HTTP POST with the given URI and payload.
     *
     * @param uri     Full URI of the remote endpoint
     * @param payload JSON-encoded data to send to the remote service
     * @return JSON-encoded result or null when there's no content returned
     * @throws RestException when an HTTP-level error occurs
     * @throws IOException   when an error reading the response occurs
     */
    public JSON post(URI uri, JSON payload) throws RestException, IOException {
        return request(new HttpPost(uri), payload);
    }

    /**
     * Executes an HTTP POST with the given URI and payload.
     * <p>
     * At least one JIRA REST endpoint expects malformed JSON. The payload
     * argument is quoted and sent to the server with the application/json
     * Content-Type header. You should not use this function when proper JSON
     * is expected.
     *
     * @param uri     Full URI of the remote endpoint
     * @param payload Raw string to send to the remote service
     * @return JSON-encoded result or null when there's no content returned
     * @throws RestException when an HTTP-level error occurs
     * @throws IOException   when an error reading the response occurs
     */
    public JSON post(URI uri, String payload) throws RestException, IOException {
        String quoted = null;
        if (payload != null) {
            quoted = String.format("\"%s\"", payload);
        }
        return request(new HttpPost(uri), quoted);
    }

    /**
     * Executes an HTTP POST with the given path and payload.
     *
     * @param path    Path to be appended to the URI supplied in the construtor
     * @param payload JSON-encoded data to send to the remote service
     * @return JSON-encoded result or null when there's no content returned
     * @throws RestException      when an HTTP-level error occurs
     * @throws IOException        when an error reading the response occurs
     * @throws URISyntaxException when an error occurred appending the path to the URI
     */
    public JSON post(String path, JSON payload) throws RestException, IOException, URISyntaxException {
        return post(buildURI(path), payload);
    }

    /**
     * Executes an HTTP POST with the given path.
     *
     * @param path Path to be appended to the URI supplied in the construtor
     * @return JSON-encoded result or null when there's no content returned
     * @throws RestException      when an HTTP-level error occurs
     * @throws IOException        when an error reading the response occurs
     * @throws URISyntaxException when an error occurred appending the path to the URI
     */
    public JSON post(String path) throws RestException, IOException, URISyntaxException {
        return post(buildURI(path), new JSONObject());
    }

    /**
     * Executes an HTTP POST with the given path and file payload.
     *
     * @param path Full URI of the remote endpoint
     * @param file java.io.File
     * @throws URISyntaxException
     * @throws IOException
     * @throws RestException
     */
    public JSON post(String path, File file) throws RestException, IOException, URISyntaxException {
        return request(new HttpPost(buildURI(path)), file);
    }

    /**
     * Executes an HTTP POST with the given path and file payloads.
     *
     * @param path        Full URI of the remote endpoint
     * @param attachments the name of the attachment
     * @throws URISyntaxException
     * @throws IOException
     * @throws RestException
     */
    public JSON post(String path, Issue.NewAttachment... attachments) throws RestException, IOException, URISyntaxException {
        return request(new HttpPost(buildURI(path)), attachments);
    }

    /**
     * Executes an HTTP PUT with the given URI and payload.
     *
     * @param uri     Full URI of the remote endpoint
     * @param payload JSON-encoded data to send to the remote service
     * @return JSON-encoded result or null when there's no content returned
     * @throws RestException when an HTTP-level error occurs
     * @throws IOException   when an error reading the response occurs
     */
    public JSON put(URI uri, JSON payload) throws RestException, IOException {
        return request(new HttpPut(uri), payload);
    }

    /**
     * Executes an HTTP PUT with the given path and payload.
     *
     * @param path    Path to be appended to the URI supplied in the construtor
     * @param payload JSON-encoded data to send to the remote service
     * @return JSON-encoded result or null when there's no content returned
     * @throws RestException      when an HTTP-level error occurs
     * @throws IOException        when an error reading the response occurs
     * @throws URISyntaxException when an error occurred appending the path to the URI
     */
    public JSON put(String path, JSON payload) throws RestException, IOException, URISyntaxException {
        return put(buildURI(path), payload);
    }

    /**
     * Exposes the http client.
     *
     * @return the httpClient property
     */
    public HttpClient getHttpClient() {
        return this.httpClient;
    }
}
